package com.example.config.security;

import com.example.util.StringUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import reactor.core.scheduler.Schedulers;

/**
 * 校验Authentication对象
 *
 * @author 王俊华
 * @date 20190829 23:20:50
 * @modify 20190829 王俊华
 */
@Component
public class JWTAuthenticationManager implements ReactiveAuthenticationManager {

    private static Logger logger = LoggerFactory.getLogger(ReactiveAuthenticationManager.class);

    @Autowired
    ReactiveUserDetailsService reactiveUserDetailsService;

    private Scheduler scheduler = Schedulers.parallel();

    @Autowired
    private PBKDF2PasswordEncoder passwordEncoder;

    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {

        if (authentication.isAuthenticated()) {
            return Mono.just(authentication);
        }

        return Mono.just(authentication)
                .switchIfEmpty(Mono.defer(this::raiseBadCredentials))
                .cast(UsernamePasswordAuthenticationToken.class)
                .flatMap(authenticationToken -> authenticateToken(authenticationToken))
                .publishOn(scheduler)
                .onErrorResume(e -> raiseBadCredentials())
                .filter(userDetails -> passwordEncoder.matches((String) authentication.getCredentials(), userDetails.getPassword()))
                .switchIfEmpty(Mono.defer(() -> raiseBadCredentials()))
                .map(u -> new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), authentication.getCredentials(), u.getAuthorities()));
    }

    private <T> Mono<T> raiseBadCredentials() {
        return Mono.error(new BadCredentialsException("Invalid Credentials"));
    }

    private Mono<UserDetails> authenticateToken(final UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken) {
        String username = usernamePasswordAuthenticationToken.getName();

        logger.info("checking authentication for user " + username);

        if (StringUtil.isNotBlank(username) && SecurityContextHolder.getContext().getAuthentication() == null) {
            logger.info("authenticated user " + username + ", setting security context");
            return reactiveUserDetailsService.findByUsername(username);
        }

        return Mono.empty();
    }
}
